Skip to content

feat(acp): enforce deterministic agent git commit identity - #6177

Open
wpfleger96 wants to merge 52 commits into
mainfrom
wpfleger/deterministic-agent-commit-identity
Open

feat(acp): enforce deterministic agent git commit identity#6177
wpfleger96 wants to merge 52 commits into
mainfrom
wpfleger/deterministic-agent-commit-identity

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 17, 2026

Copy link
Copy Markdown
Member

Problem

Agent sessions could commit under the human operator's ambient git identity. Only buzz-dev-mcp's shim applied the nostr author/signing GIT_CONFIG_* env, and only to its own shell-tool children — the native shells of claude-code, codex, and goose never saw it. A bare git commit in those shells resolved to whatever user.name/user.email the repo or global config carried, erasing the AI-attribution signal (e.g. #3140, which landed under a human identity).

Change

Makes the agent commit author identity machine-managed and deterministic across every harness. Trailers still credit the human operator (Co-authored-by + Signed-off-by); that policy is unchanged.

  • buzz-git-identity (new crate) — the single source of truth for the author/email, NIP-GS signing, and keyfile logic, consumed by both the dev-mcp shim and the harness so an agent commits under a byte-identical identity regardless of which surface applied it. The identity email is <64-hex-pubkey>@<relay-host>; the author name is the sanitized Buzz display name (falls back to the npub).
  • Harness-owned identity authority. Alongside the keyfile, the harness/shim write a 0600 identity manifest. The enforcement wrapper locates its own install directory by canonicalizing the git on PATH against its own executable — the same trust channel it uses to find the real git, immune to environment rewriting — reads the expected identity from the manifest, and injects it directly as the highest-precedence config, never trusting git config user.email. A manifest that is missing or fails to parse in an otherwise-managed session fails the invocation closed rather than silently dropping enforcement. Manifest present = enforce; genuinely absent (keyless/unconfigured session) = passthrough.
  • git enforcement wrapper (git_wrapper.rs) — installed on PATH ahead of the real binary.
    • Identity is injected, not merely scrubbed. The wrapper injects the manifest's identity + signing config as -c args at the front of the resolved command, so it outranks caller-supplied -c, GIT_CONFIG_PARAMETERS, include.path, and repo/global config alike. Scrubs GIT_AUTHOR_*/GIT_COMMITTER_*; rejects -c user.*, --config-env=user.*, --author, and --reset-author (scoped to commit/am so git log --author still works).
    • Author preflight. commit -C/-c <sha> and --amend create new commits that reuse another commit's author; injected config cannot override a reused author, so the wrapper inspects the resulting author and rejects any commit-creating mode that would leave a non-agent author. Ordinary commits and amends of the agent's own commits pass.
    • Push gate uses git's own resolved plan. Rather than predicting git's transport grammar from argv, the wrapper runs git push --dry-run --porcelain --no-verify <original args> to obtain the exact resolved update set — covering config-defined and inline -c alias.* git aliases, -C/--git-dir, --all/--mirror/--tags, config remote.*.push, and wildcard refspecs — then refuses any outgoing commit not authored by the agent identity, naming the offending sha and email. A human-authored commit is exempted only when it is a patch-id-identical replay of a commit already upstream (a legitimately cherry-picked/rebased human commit — correct attribution, not new agent work masquerading as someone else); any other non-agent author is refused. The dry-run probe is bounded by a hard timeout; a timeout, an unresolvable update set, or any dry-run failure fails closed. The --no-verify on the internal dry-run keeps it from double-running the repo's own pre-push hooks; the real push keeps its hooks, and enforcement runs unconditionally so --no-verify on the real push cannot bypass it.
    • Aliases are allowlisted, not blocklisted. A git alias is expanded by git in-process, and its config-bearing globals land after the wrapper's injected identity/signing -c options, so an alias could otherwise plant higher-precedence config that re-authors or unsigns the commit — and git's quote-aware alias parser sees tokens differently from a naive whitespace scan ('-c' 'user.email=…' dequotes to real config). Rather than model that grammar, the wrapper admits a non-shell alias only when every token of its resolved body is a trivially-safe bare word: no quote or backslash characters, no -c/--config-env channel, and no =-valued option. Anything else is refused. Shell (!) aliases are refused outright in a managed session — git runs their body with the real git ahead of the wrapper on PATH, so an inner -c outranks the inherited authority and can commit or push under an arbitrary identity, unsigned; there is no safe subset to allow. A bare-word alias can still carry identity/signing flags (commit --author … --no-gpg-sign), so on success the alias is expanded and its resolved command — accumulated body tokens across up to ten alias substitutions plus the caller's trailing argv; if another alias remains at that bound, the wrapper refuses rather than treating a partial expansion as resolved — is held to the identical identity/signing policy as the same command typed directly (enforce() and the commit-author preflight, keyed on the expanded subcommand). An alias can therefore never do more than its expansion could typed directly, and there is no alias-specific flag list to keep in sync. Bare-word aliases whose expansion is clean keep working (alias.ci = commit, alias.st = status, alias.lg = log --oneline, alias.pub = push origin main).
    • Signing is enforced. -c commit.gpgSign=false and --no-gpg-sign are rejected at argv; env-based signing-disable is defeated by the injected highest-precedence config.
  • Harness lift (buzz-acp) — AcpClient::spawn writes the keyfile and manifest, installs the wrapper plus the nostr signer/credential helpers via buzz-acp's own multicall personalities, prepends the wrapper dir to the child PATH, and applies the identity + signing GIT_CONFIG_* composed over the desktop's per-URL credential helper. The key is sourced BUZZ_PRIVATE_KEY (the documented required secret) before NOSTR_PRIVATE_KEY at both the command and process-env layers, and the canonical key is restaged unconditionally as the child's NOSTR_PRIVATE_KEY so the harness and dev-mcp shim can never install split identities. A managed session fails closed when deterministic identity cannot be installed; sessions with no nostr key are skipped entirely, so test spawns and unconfigured sessions are unchanged. Unix-only.
  • Prompt guidance (base_prompt.md, nest_agents.md) — identity is machine-managed; credit the operator via Co-authored-by/Signed-off-by trailers, never user.name/user.email/-c/--author.
  • Sovereignty toggle (BUZZ_GIT_IDENTITY=agent|user, default agent, settable per-agent via persona env). agent is everything above. user reuses the existing review-hardened unconfigured-session path — the harness installs no wrapper on PATH, no manifest, no keyfile, and no injected authorship/signing config, so vanilla git resolves the operator's own repo/global identity and signing. This matches VISION_SOVEREIGN.md: the operator, not the platform, decides whose identity their agent's commits carry on their own machine. The nostr credential helper (relay git-over-HTTP auth) keeps installing in user mode, and the harness still stages the canonical key as the child's NOSTR_PRIVATE_KEY before its user-mode early return so the helper can authenticate on every launch path (headless as well as desktop) — auth is not attribution, and setting user on a configured session must not silently drop relay git auth. The mode is read once at spawn by the harness and dev-mcp shim, never by the wrapper per-invocation, so an agent cannot export BUZZ_GIT_IDENTITY=user mid-session to disable enforcement. An unrecognized value fails the spawn loudly, naming the variable and its two values, rather than silently choosing a mode. user mode drops the operator out of commit-level AI attribution — deliberate, their claim to make; in that mode the Co-authored-by/Signed-off-by trailers are redundant since the commit already is the operator's identity.

Scope and ceiling

This is a deterministic best-effort local control, not an adversarial sandbox. Enforcement is a PATH wrapper sharing the OS user with the agent, so it closes accidental identity leakage — the entire class behind #3140 — but does not stop a deliberate bypass: invoking /usr/bin/git by absolute path, env -i, replacing the wrapper on PATH, deleting the manifest, or committing via libgit2/jj all sidestep it by design. A hard guarantee that holds regardless of what the agent runs would require a receive-side gate on the git host; that remains a possible separate follow-up. This PR intentionally targets the default-path commit/push surface, which is where the missed-attribution signal originates.

Verification

Beyond unit tests (buzz-git-identity 66, buzz-acp 817 unit + integration suites), the wrapper mechanism is exercised end-to-end by process-level tests (git_identity_enforcement.rs) that spawn the real buzz-acp-as-git multicall against a real repo with a manifest present:

  • a flag-based identity override (-c user.email=…, --author=…) is rejected;
  • agent identity is injected over conflicting repo config so the resulting commit is authored <display-name> <hex@relay>;
  • a quote-obfuscated config alias (alias.quoted = '-c' 'user.email=…' commit) and a shell (!) commit alias are each refused before git runs and leave HEAD unchanged, while a plain-subcommand alias still resolves and commits agent-authored; a bare-word alias carrying identity/signing flags (alias.human = commit --author … --no-gpg-sign, --no-gpg-sign alone, and a two-hop chain) is expanded and refused by the same policy as the typed command, HEAD unchanged; a chain of exactly ten aliases reaching commit remains usable, while an eleventh alias is refused before git runs with unborn HEAD and zero commit objects;
  • a push containing a human-authored commit is refused via git's resolved plan, while an agent-authored push is allowed;
  • the full spawn path installs the wrapper + manifest so a plain agent commit in a human-configured repo lands agent-authored.

Each process-level test is mutation-verified: nulling the harness's install_git_identity wiring, or dropping the enforce/verify_push dispatch, turns the corresponding test red — confirming the layer is wired into the process boundary, not merely unit-covered. The key-precedence and bounded-probe paths carry their own mutation-sensitive unit tests. The keyfile-lifecycle test (keyfile_lifecycle.rs) spawns the real binary on its error-exit path and confirms the 0600 keyfile is deleted on every exit.

The BUZZ_GIT_IDENTITY toggle is covered at three layers: GitIdentityMode parsing unit tests (unset → agent, exact agent/user with whitespace tolerance, and loud rejection of typos/case-variants/empty naming the variable and both values); harness gate tests (user installs no identity dir or GIT_CONFIG_*, a persona-staged value outranks process env, explicit agent still writes the manifest, an invalid value errors at spawn); and shim seam tests (agent installs the git wrapper + signer + manifest + authorship config; user keeps only the git-credential-nostr helper and its nostr.keyfile pointer with no wrapper, signer, manifest, or authorship/signing config; unset defaults to agent; invalid fails install).

Notes

  • Squash-merge re-authors the merged commit to the PR-opener's GitHub token regardless of this change; the durable merged-history attribution signal is the Co-authored-by trailer email.
  • NIP-GS signatures render as "unverified" on GitHub's UI (GitHub has no nostr x509 trust root); the signature is still verifiable via git-sign-nostr.

Agent sessions could commit under the human operator's ambient git
identity: only buzz-dev-mcp's shim applied the nostr author/signing
GIT_CONFIG_* env, and only to its own shell-tool children. The native
shells of claude-code, codex, and goose never saw it, so a bare
`git commit` there resolved to whatever the repo/global config carried
— erasing the AI-attribution signal (e.g. block/buzz #3140).

Make the identity machine-managed across every harness:

- New `buzz-git-identity` crate holds the pure author/email/signing/
  keyfile logic as the single source of truth, consumed by both the
  shim and the harness so an agent commits under a byte-identical
  identity regardless of which surface applied it.
- A `git` enforcement wrapper (installed on PATH ahead of the real
  binary) scrubs GIT_AUTHOR_*/GIT_COMMITTER_* from the child env,
  rejects `-c user.*`, `--config-env=user.*`, `--author`, and
  `--reset-author`, and on push refuses any outgoing commit not
  authored by the agent identity, then execs real git.
- The harness lifts the identity + NIP-GS signing config onto the
  agent-runtime child and installs the wrapper plus the nostr
  signer/credential helpers via buzz-acp's own multicall, so native
  shells of all runtimes inherit both. Composed over the desktop's
  per-URL credential helper; skipped when no nostr key is present.
- Prompt guidance (base_prompt.md, nest_agents.md) updated: identity
  is machine-managed; credit the operator via Co-authored-by/
  Signed-off-by trailers, never user.name/email/-c/--author.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 17, 2026 23:03
Duncan and others added 19 commits August 18, 2026 10:25
Round-1 review fixes on the deterministic agent-commit-identity work:

- L3 push gate resolved the effective command through git aliases
  (config-defined and inline -c alias.x=push) so a push disguised as a
  custom alias can no longer skip outgoing-author verification.
- Verification subprocesses now carry repository context (-C, --git-dir,
  --work-tree, --namespace); an outgoing tip that resolves to a real ref
  but whose range cannot be computed fails closed instead of being
  skipped as nothing-to-check.
- AcpClient::shutdown deletes the git-identity keyfile tempdir explicitly
  via TempDir::close before the process-group kill. Relying on Drop right
  before std::process::exit leaked the 0600 nostr keyfile ~80% of runs;
  all client-owning error/timeout exits funnel through shutdown_and_exit,
  which takes the client by value.

Checkpoint commit: the review round continues on this branch with the
identity-authority and push-boundary rework.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The enforcement wrapper trusted the same caller-mutable GIT_CONFIG_*
environment it was meant to constrain, so `env -u GIT_CONFIG_COUNT git
commit` fell back to repo-local human identity and the push gate derived
its expected identity from that same mutable config and failed open.

- Authority: harness/shim write a 0600 identity manifest beside the
  keyfile; the wrapper locates its own install dir by PATH
  canonicalization, re-applies identity+signing GIT_CONFIG_* at the
  highest index before exec, and reads L3's expected author from the
  manifest. Manifest present = enforce; absent = passthrough.
- L1b eligibility sources the key from BUZZ_PRIVATE_KEY then
  NOSTR_PRIVATE_KEY, decoupled from credential-helper discovery, and
  fails the managed session closed when identity cannot be installed.
- Push gate uses git's own resolved plan (push --dry-run --porcelain
  --no-verify) instead of predicting argv, covering aliases, -C,
  --all/--mirror/--tags, config refspecs; unresolvable = fail closed.
- Author preflight rejects commit -C/-c/--amend that would leave a
  non-agent author; rebase/cherry-pick of upstream history pass.
- Signing-disable via -c/--no-gpg-sign rejected at argv.
- Process-level mutation tests spawn the real multicall and go red when
  each enforcement layer is removed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  Polish mobile timeline navigation (#5874)
  chore(release): release Buzz Desktop version 0.5.17 (#6234)
  fix(prompt): simplify pickup follow-through (#6186)
  fix(mcp): scope todo usage (#6216)
  fix(desktop): bound remote agent mention authorization (#6224)
  fix: bump h2 for RUSTSEC-2026-0258 (#6222)
  fix(desktop): bind presence retry timers (#6213)
  ci: make file-size policy a first-class gate (#6187)
  fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198)
  chore(release): release Buzz Desktop version 0.5.16 (#6191)
  fix(desktop): restore release agent mentions (#6182)
  test(desktop): cover exact workflow batch limit (#6168)
  chore(release): release Buzz Desktop version 0.5.15 (#6173)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity

* origin/main:
  fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity

* origin/main:
  feat(managed-agents): close five Claude Code agent-config gaps (#4557)
  chore(hooks): keep mobile analysis out of pre-commit (#6236)
  fix(shared-ui): delay hover disclosures by default (#5821)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Address the reconciled Thufir/Gurney round-2 findings on the deterministic
agent git-identity gate:

- Injected identity args now outrank caller `-c`/GIT_CONFIG_PARAMETERS/
  include.path/repo config, and a tampered manifest fails closed (C1).
- Shell (`!`) push aliases are treated as opaque and rejected before any
  probe, so an alias cannot transmit before verification (C2).
- Author-preserving commit modes gain a patch-id exemption so legitimate
  rebases of upstream human commits pass while new human-authored commits
  are still refused (I3).
- BUZZ_PRIVATE_KEY outranks NOSTR_PRIVATE_KEY at both layers and the
  canonical key is restaged unconditionally as the child NOSTR_PRIVATE_KEY,
  so the harness and dev-mcp shim can never install split identities (I5).
- The push predictor's subprocess probe is bounded by a hard timeout and
  fails closed on expiry (I6).

Adds a real-binary integration test driving the buzz-acp spawn path so the
install_git_identity wiring is regression-covered, plus mutation-sensitive
unit tests for the key precedence and timeout paths.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  Refine mobile pairing confirmation (#6018)
  chore(scripts): add buzz-adopt-prod-agents.sh (#6250)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
An ordinary (non-shell) git alias whose body carries global `-c`/`--config-env` config is expanded by git in-process after the wrapper's injected identity/signing `-c` options, so the alias-added config outranks the authority and could silently re-author or unsign a commit. enforce() inspects only the literal argv and never the alias body, so it could not catch this — a repo-local alias recreated the original human-attribution leak through the managed wrapper.

Add a pre-exec verify_alias_safety pass that resolves the effective alias chain and refuses any alias whose expansion introduces global configuration, per the favor-rejection principle rather than modeling git's full alias grammar. Plain-subcommand aliases keep working. Shell (`!`) aliases stay out of scope here: their git invocations re-enter the wrapper on PATH and push-bearing ones are already rejected as opaque.

Also gate the unconditional `nostr::ToBech32` import in git_identity_enforcement.rs behind #[cfg(unix)] (its only use is a unix-only test) so the Windows clippy gate stops failing on unused-import under -D warnings.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…shell aliases

The prior blocklist scanned an alias body for known-bad config tokens, but git's quote-aware alias parser dequotes `'-c' 'user.email=…'` into a live config channel that a naive whitespace scan never sees — a parser-parity bypass. And `!` shell aliases were treated as safe on the commit path on the false premise that their inner git re-enters this wrapper; git prepends its own exec-path to PATH, so the inner git is the real binary and its -c outranks the inherited env authority, committing as an arbitrary human, unsigned.

Invert verify_alias_safety to an allowlist: a non-shell alias is admitted only when every body token is a trivially-safe bare word (no quote/backslash, no -c/--config-env in any spelling, no =-valued option); anything else is refused without modeling git's grammar. Reject all shell aliases outright in a managed session, commit path included. This makes the whole config-injection class end by construction. Gurney's certified shapes (ci=commit, st=status, lg=log --oneline, pub=push origin main) stay allowed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311)
  fix(desktop): morph the drawer panel icon instead of sliding it (#6306)
  feat(desktop): refine repository-aware project workspaces (#6003)
  Fix mobile Activity thread navigation (#5850)
  perf(desktop): parallelize relay agent directory rebuild (#6258)
  Refine the mobile emoji picker (#5853)
  fix(desktop): exclude archived agents from nest, order regeneration (#5905)
  Add font size and conversation density preferences (#5644)
  fix(desktop): emit camelCase config-write payload fields (#6062)
  fix(desktop): downscale large avatars for agent-share PNG body (#6260)
  fix(desktop): preserve early relay auth challenges (#3320)
  Polish mobile message actions (#5873)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
… policy

A bare-word alias whose body carried identity/signing flags (`commit --author … --no-gpg-sign`) passed the allowlist because every token is a plain bare word, and enforce()/verify_commit_author() keyed on the literal typed subcommand (the alias name, never the expanded `commit`) — so the flag preflights never fired. A repo-local alias through the managed wrapper could author as a human and disable signing.

verify_alias_safety now returns the alias's fully-resolved expansion (typed globals + recursively-expanded command with accumulated body tokens and the user's trailing argv). run() holds that expansion to the same enforce() and verify_commit_author() preflight as a directly-typed command, keyed on the expanded subcommand. An alias can no longer do more than its expansion could typed directly, so there is no alias-specific flag list to maintain. Shell-alias and unclassifiable-syntax refusals are unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  fix(buzz-acp): loosen workspace-scan guardrail to allow named paths (#6261)
  fix(buzz-dev-mcp): expand leading ~ in read_file/str_replace paths (#6271)
  perf(desktop): move five hot renderer paths from JS into Rust (#6024)
  fix(media): accept portrait video resolutions (#6058)
  fix(desktop): hide archived channels from #/Tab autocomplete (#6156)
  Unify mobile channel details (#6113)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Git continues resolving aliases after the wrapper reaches its bounded expansion limit. Refuse when the next command word remains an alias so a partial expansion cannot bypass managed identity and signing policy.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tity

The deterministic-agent-commit-identity enforcement is unconditional: an
agent always commits under its nostr identity. Sovereignty (VISION_SOVEREIGN.md)
says the operator, not the platform, decides whose identity their agent's
commits carry on their own machine.

Add BUZZ_GIT_IDENTITY=agent|user (default agent, settable per-agent via
persona env). `user` reuses the existing review-hardened unconfigured-session
path — no git wrapper on PATH, no manifest, no injected authorship/signing
config — so vanilla git resolves the operator's own identity. The nostr
credential helper (relay git-over-HTTP auth) keeps installing in user mode:
auth is not attribution. The mode is read once at spawn by the harness and
shim, never by the wrapper per-invocation, so an agent cannot disable
enforcement mid-session. An unrecognized value fails the spawn loudly rather
than silently choosing a mode.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main: (40 commits)
  chore(release): release Buzz Desktop version 0.5.18 (#6489)
  fix(desktop): simplify duplicate agent provenance (#6401)
  test(benchmarks): expand Buzz-native dataset (#6448)
  fix(desktop): sender names in notifications + macOS click-through routing (#6427)
  docs: clarify two-layer moderation ownership (#6481)
  Fix mobile thread tail and iOS channel header (#6399)
  chore(deps): pin earshot below 1.2.0 pending a VAD threshold re-pick (#6392)
  polish(desktop): finish Projects navigation and context chrome (#6429)
  fix(desktop): clarify add agents channel action (#6374)
  Repair stale large channel roster snapshots (#6251)
  feat(desktop-messages): show compact Buzz link metadata (#6252)
  feat(workflows): reply in-thread from send_message action (#6178)
  perf(desktop): split discover_acp_providers into cheap and forced paths (#6330)
  fix(desktop): restore recent channel sorting (#6402)
  fix(desktop): isolate main timeline stacking context from focus drawer (#6398)
  fix(desktop): make reconnect repair lossless (#6415)
  fix(hooks): scope pre-push lanes to branch merge-base diff (#6423)
  Enforce a three-day dependency cooldown (#6426)
  perf(desktop): resolve references without directory scans (#6328)
  feat(llm): stamp thinking effort on call-completed log line (#6424)
  ...

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lper

In BUZZ_GIT_IDENTITY=user mode the harness returned Ok(None) before
resolving the agent key, so a headless BUZZ_PRIVATE_KEY-only launch left the
dev-mcp shim's credential-helper-only branch with no key — the toggle silently
removed relay git AUTH, not just attribution. auth != attribution must hold on
every launch path, and an operator who set user mode on a configured session
has not unconfigured their auth.

Resolve the canonical key (same BUZZ_PRIVATE_KEY-over-NOSTR_PRIVATE_KEY,
child-over-process precedence) and stage it as the child's NOSTR_PRIVATE_KEY
BEFORE the user-mode early return. user mode still installs no wrapper,
manifest, keyfile, or injected config on the harness side; no key at all is
still Ok(None). Wrapper unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The extra_env merge in AcpClient::spawn only staged a persona value when
the parent process env lacked that key, so a global BUZZ_GIT_IDENTITY silently
defeated every per-agent override in both directions — the child-over-process
lookup in install_git_identity never saw the dropped value. Treat the var as an
operator-controlled exception and always stage the persona value; the general
parent-wins semantics for other keys are unchanged.

Also derive Debug on Shim so the shim seam test's expect_err compiles under
clippy -D warnings (--workspace --all-targets).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  fix(desktop): restore human barge-in over agent TTS in huddles (#6431)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The test mutated process-global env across .await points and unconditionally
removed the variable, clobbering any caller-supplied value instead of restoring
it. Add an RAII guard that captures the prior value and restores-or-removes on
drop (including on assertion panic). It is the only env-mutating spawn test in
buzz-acp, so no cross-test serialization is required. Both-direction
real-merge-loop assertions are unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

I found three blocking contract failures at c64e62e599ffaaf223a111c34ea600d341967f57:

  1. P1: managed git can create and push unsigned agent-authored commits. enforce() rejects --no-gpg-sign for commit|am|tag|rebase|cherry-pick|revert, but not merge/pull; the outgoing gate then checks only author email and never verifies signature presence or the expected key. A focused real-wrapper reproduction created an unsigned merge with git merge --no-gpg-sign --no-ff, observed %G? = N, and pushed it successfully. git commit-tree produces the same systemic bypass through the installed wrapper. This contradicts the PR’s “every commit is automatically signed” contract. At minimum cover every signing-capable porcelain path; the robust boundary is to require a valid expected-key NIP-GS signature on each new agent-authored outgoing commit, with merge/plumbing push regressions.

  2. P1: BUZZ_GIT_IDENTITY=user can erase relay git authentication through descriptor env. Desktop stages its credential-helper GIT_CONFIG_* entries and then writes descriptor.env afterward (runtime.rs:781-811). A descriptor with BUZZ_GIT_IDENTITY=user plus GIT_CONFIG_COUNT=0 reaches the harness; user mode intentionally returns before installing identity config, leaving the clobbered count in place. That violates the explicit auth-is-not-attribution guarantee. Preserve/reapply the helper after descriptor env or reserve the complete GIT_CONFIG_* family, and test the actual Desktop-to-harness layering rather than a fresh Command.

  3. P2: the changed Nest guidance is not delivered to existing installs and conflicts with #6707. This PR rewrites nest_agents.md but leaves NEST_AGENTS_VERSION at 4 and adds no content/upgrade test, so existing v4 Nests never receive it. #6707 bumps 4→5, tests refresh/preservation, and intentionally removes the unconditional human Signed-off-by mandate that #6177 retains. The two branches edit the same block and #6707’s content test rejects #6177’s exact wording. Reconcile onto one policy contract; keep #6707’s version bump and upgrade coverage rather than landing both as-is.

The runtime mechanism in #6177 is broader than #6707: #6707 is policy-neutral generated guidance and does not replace identity enforcement. Its wording already allows a managed agent identity while keeping repository policy authoritative, so it is the safer documentation base to combine with a corrected runtime implementation.

Current CI for #6177 is green and git diff --check passes, but neither covers these boundary failures.

Duncan and others added 2 commits August 25, 2026 10:45
A user-supplied GIT_CONFIG_* entry (persona/agent/global env_vars) is
layered onto the spawn command AFTER the relay credential-helper
GIT_CONFIG_* Buzz stages, so a single GIT_CONFIG_COUNT=0 silently orphans
the helper. In BUZZ_GIT_IDENTITY=user mode install_git_identity returns
before re-staging anything, so the clobber stands and relay git auth is
erased — violating the auth-is-not-attribution guarantee.

Reserve the whole GIT_CONFIG* family (bare name + GIT_CONFIG_ prefix) in
the shared is_reserved_env_key predicate. This is the single chokepoint
every env layer already routes through (save-time validation, spawn-time
filter, and the remote-deploy launch env), closing both the local and
headless paths at once.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main: (54 commits)
  Extract community persistence (#6668)
  Fix mobile Huddle agent voice turn states (#6611)
  Add inline profile camera capture (#6680)
  Hide Huddles in mobile agent DMs (#6676)
  fix(desktop): polish inline chip states (#6718)
  Centralize replaceable event persistence (#6660)
  feat(workflows): discover trigger filter values (#6712)
  feat(desktop): simplify the message action rail (#6529)
  fix(desktop): restore icon-only remote marker (#6491)
  fix(ci): prevent poisoned Rust caches (#6618)
  docs(security): route reports through private advisories (#6728)
  fix(composer): wrap Buzz chip labels without orphaning icons (#6581)
  fix(desktop): bound thread /query and surface load errors, not false-empty (#6447)
  fix(messages): route edits to the owning composer (#6575)
  fix(mobile): join starter channels after accepting invite (#5915)
  Add mobile profile editing (#6583)
  fix(desktop): align jump-to-latest pill with composer height (#6606)
  fix(desktop): emit singular `mention` feed category so alerts route correctly (#6665)
  fix(mobile): recover stale and shuffled messages (#6691)
  feat(mobile): browse and join open channels (#6243)
  ...

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wesbillman added a commit that referenced this pull request Aug 25, 2026
## Summary

- replace the generated Nest's unconditional human author/sign-off rules
with portable guidance that separates authorship, material
co-authorship, DCO certification, and cryptographic signing
- defer attribution to repository-local policy, forbid inferred or
guessed identities, and require inspection of every outgoing commit
- bump the Nest template version so existing installations refresh, with
regression coverage for fresh generation and upgrade preservation

### Related issue

None found. Related runtime identity work exists in #6177, but this PR
is intentionally limited to the generated Nest guidance and its refresh
behavior.

### Testing

- `bin/just desktop-tauri-clippy`
- `bin/just desktop-tauri-test`
- `bin/just file-size-check`
- `git diff --check`
- pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`,
and `desktop-tauri-checks`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Duncan added 3 commits August 26, 2026 10:47
…c-agent-commit-identity

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…sh gate

Unsigned agent-authored commits created via git merge/pull --no-gpg-sign
or the commit-tree plumbing sail past the flag-based enforce() rejection,
which only covers commit/am/tag/rebase/cherry-pick/revert on the literal
argv. Extend the existing verify_push walk — which already visits every
outgoing commit not on a remote and checks author email — to also require
a valid NIP-GS signature by the agent key on each agent-authored commit,
gated on the session actually enforcing signing (commit.gpgSign=true).
One check then covers every commit-creation path, including plumbing,
without growing the enforce() blocklist.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  fix(cli): preserve signatures in event reads (#6884)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
@wpfleger96
wpfleger96 force-pushed the wpfleger/deterministic-agent-commit-identity branch from ff6a3e4 to d157e94 Compare August 26, 2026 15:32
@wpfleger96
wpfleger96 requested a review from wesbillman August 27, 2026 17:18

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

P1: caller-supplied config can introduce an alias that bypasses both alias safety and push verification. verify_alias_safety only recognizes exact lowercase inline -c alias.* definitions, then probes config using repository-context arguments only; is_push_command follows the same incomplete view (crates/buzz-git-identity/src/git_wrapper.rs:370-417,458-520). But enforce permits config keys outside its protected identity/signing list (:588-605,666-678). Therefore git -c include.path=/tmp/evil x, where the include defines alias.x = !git -c user.email=human … commit --no-gpg-sign, is treated as a real non-push command and handed to Git. Git then resolves the newly introduced shell alias, whose inner git bypasses the wrapper exactly as the threat model at :447-451 describes. --config-env=alias.x=ENV and case-varied -c ALIAS.x=… take the same path; an injected alias resolving to push also skips outgoing author/signature verification.

Resolve aliases under the caller’s complete effective config without executing them, or fail closed on every config channel that can introduce aliases/includes. Add commit and push regressions for include.path, --config-env aliases, and case variants. The earlier keyless and headless-helper lifecycle blockers are fixed at this head, but this reopens the unsigned-commit/push trust-boundary failure.

Review was read-only against exact head ba05511a843c8635b5e300f2ff33e61ed219e00f; I did not check out or execute PR code. Exact-head standard CI is green.

…smuggling

The alias-safety and push-verification probes read `git config --get
alias.<name>` under repo-context globals only, so an alias introduced through
the caller's own config channels was invisible to them: `git -c
include.path=<f> x` (file defines `alias.x = !git … commit`),
`--config-env=alias.x=VAR`, and case-varied `-c ALIAS.x` all classified as
real non-alias commands and reached the real git, which resolved the alias and
bypassed both the shell-alias refusal and outgoing-author verification.

Resolve every probe under the caller's complete config-injecting globals
(`-c`/`--config-env`, attached and split forms) via `alias_probe_ctx`, so the
probe sees exactly the alias set git will expand. Include/config-env/case
variants now hit the existing `!`-refusal and bare-word allowlist with no new
policy. This makes the hand-rolled `inline_aliases` map redundant (git's own
resolution gives correct inline-over-file precedence and case-insensitive
section names), so it is removed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The alias-safety and push-detection probes resolved `git config --get
alias.<name>` under an allowlist of context globals (repo-context in
round 4, plus the config-injection channels in round 6). A caller
global outside that allowlist that changes which config git reads was
therefore invisible to the probe while the real invocation still applied
it. `--bare` is the concrete gap: it changes repository discovery, so
`git -C <dir> --bare x` can expand a shell/push alias defined only in the
bare view — one the probe, resolving the non-bare view, never sees —
bypassing the shell-alias refusal and the outgoing author/signature gate.

Replace the allowlist with the complete caller global set: the probe
context is now every global token before the subcommand, so each probe
resolves under the exact repository and configuration git will use. This
closes the class rather than the instance — any global that would
corrupt a probe corrupts the real invocation identically, so probe and
real git share one view and fail closed together. The invariant is
documented at the definition so it is not re-narrowed. The context also
now feeds the push-verification probes (rev-list/show), which had the
same dropped-`--bare` divergence from the push being verified. Authority
identity/signing `-c` entries still splice last, so caller config cannot
outrank them.

Removes the redundant `repo_context_args`/`config_probe_globals`/
`alias_probe_ctx` split; adds real-wrapper commit + push regressions for
a `--bare`-selected alias.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
split_globals is the single point of truth for where the subcommand
begins, and every alias/push probe resolves under the globals it extracts.
git 2.54 treats --shallow-file as a separate-value global, but the table
omitted it: git -C <repo> --shallow-file -c x makes real git consume -c as
the shallow-file path and dispatch alias x, while the wrapper ate x as the
value of -c, found no subcommand, and skipped every preflight — reopening
the shell-alias and push-signature bypass.

Add --shallow-file to VALUE_LONG_OPTS with a version-pinned comment, a
grammar test pinning every git 2.54 separate-value global, and real-wrapper
shell/push regressions using the option-like-value shape.

Also repair 12 alias unit tests that regressed silently at the round-6
refactor: probe-context derivation moved from inside the probes to the run
caller, but these tests still passed ctx=vec![] with their alias config
inline in argv, so they probed under an empty context. Build ctx via
caller_globals(argv), matching production. This crate is absent from the
CI test-unit -p list, so the regression never surfaced in CI.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Two blocking defects remain at exact head e8cb5dab193521e8e8888508b1c1a3c75edf2426:

  1. P1: a forged local remote-tracking ref bypasses all outgoing author and signature checks. rev_list_outgoing computes candidates as rev-list <source> --not --remotes, but refs/remotes/* is caller-writable local state, not evidence that a commit exists at the push destination. An agent can create an arbitrary wrong-author or unsigned commit, run git update-ref refs/remotes/forged/main HEAD, and then push it. The dry-run plan still names the pushed source, but the rev-list is empty, so both verification loops accept without inspecting HEAD. Derive exclusions from commits positively established at the actual destination, fail closed when that set cannot be established, and add a real-wrapper regression for the forged-ref case.

  2. P1: agent-mode ACP overwrites Desktop’s staged per-relay credential config. Desktop stages GIT_CONFIG_COUNT=2 and indices 0–1 directly on the child Command. install_git_identity detects that helper and declines to add another, but to_git_config_env chooses its append index only from the harness process environment. In the normal Desktop-to-harness path that base is zero, so agent identity entries replace the child command’s count and indices 0–1, orphaning credential.<relay>/git.helper and breaking relay Git authentication. Compose against the actual child-command environment (or explicitly preserve/reapply those entries) and add an agent-mode Desktop layering regression; the current Desktop-helper test covers only the early-returning user mode.

The latest alias/config fixes otherwise address the prior include/config-env/case-variant and --shallow-file parser gaps, but these two defects still violate the core deterministic identity and authenticated-push contract.

Duncan and others added 2 commits August 28, 2026 15:09
… config on the child

The push gate excluded commits reachable from refs/remotes/*, which is
caller-writable: an agent could update-ref a forged remote-tracking ref
to HEAD and hide an unsigned or wrong-author commit from both
verification loops. Read the destination git resolved on the porcelain
To line and derive exclusions from its real object ids via ls-remote,
failing closed when the destination or the remote read is unavailable.

Agent-mode ACP composed its GIT_CONFIG_* append index from the harness
process env, but Desktop stages the per-relay credential helper directly
on the child command. Compose against the count the child will actually
see (child-staged, else inherited) so the helper survives instead of
being overwritten at index 0.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  feat(desktop): add team sharing to community catalog (#3995)
  Refresh mobile utility surfaces and theme picker (#6944)
  fix(desktop): complete project empty and context states (#6980)
  Fix mobile jump-to-latest flicker (#6807)
  refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777)
  refactor(db): split channel membership store (#6782)
  feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776)
  fix(desktop): resolve exact typed mentions on space (#6862)
  perf(desktop): restore project context during startup (#6939)
  fix(desktop): lift right auxiliary pane above shared header backdrop (#6966)
  fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962)
  feat(desktop): implement 30178 team catalog backend (#5112)
  feat(model-capabilities): humanize Databricks UC model families (#6955)
  feat(agent): discover Databricks Unity Catalog models (#6918)
  test(db): use canonical channel roster fixtures (#6819)
  preserve channel description paragraph breaks (#6946)
  fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825)
  Fix Codex security review authorization (#6913)
  fix(db): disable heartbeat vacuum truncation (#6898)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

One blocking trust-boundary defect remains at exact head c402144dd1dc215758071c131d684f2c970c5322:

P1: a caller-controlled receive-pack redirects the real push away from the remote that supplies the exclusion set. resolve_push_sources records the porcelain To <dest> label as the destination (lines 955-985), then remote_object_ids runs a separate git ls-remote <dest> and trusts those object IDs as proof that commits are already present (lines 1249-1271). But git push supports --receive-pack / --exec, plus remote.<name>.receivepack, and the To label does not encode that override. The dry-run and real push therefore use the custom receive-pack while the exclusion probe does not.

I reproduced this with vanilla Git and no PR code: create a wrong-author commit; seed a decoy bare repo with that SHA; leave a second bare repo empty; configure a remote whose URL is the decoy and whose remote.<name>.receivepack helper executes git-receive-pack against the empty repo; then push the commit through that remote. Porcelain reports To <decoy>, ls-remote <decoy> returns the bad commit, and rev-list HEAD --not <that SHA> is empty. The real push invokes the configured receive-pack and successfully writes the unchecked commit to the second repo. The same bypass is available through the documented --receive-pack / --exec flags.

Reject custom receive-pack configuration and flags in managed mode, or otherwise make the exclusion proof use the exact effective push transport and endpoint. Add a real-wrapper regression where the advertised To repository already contains the offending SHA but the receive-pack target does not.

The previous forged refs/remotes/* and Desktop credential-composition blockers are fixed at this head. The remaining lifecycle, alias/config grammar, and ordinary push-plan paths reviewed cleanly, but this redirect still breaks the core guarantee that every outgoing commit is checked against the actual destination.

Duncan and others added 10 commits August 29, 2026 14:24
…n-sensitivity

- Fix verify_push_rejects_multiple_pushurls: add both A and B as explicit
  pushurls (remote set-url --add --push twice); a single --add only gives
  one push destination since git ignores remote.origin.url once any pushurl
  exists.

- Fix verify_push_rejects_insteadof_rewrite_replay: use pushInsteadOf+insteadOf
  chain so To header names A (push dest) while ls-remote --get-url A returns B
  (insteadOf rewrite) and B holds HEAD. Guard catches A≠B and refuses. Plain
  insteadOf rewrites both push and fetch so To already shows B, defeating the
  test. Seed B with HEAD for mutation-sensitivity: without verify_destination_stable
  ls-remote A→B returns HEAD's IDs, exempting it, and A receives the commit.

- Fix integration tests (wrapper_refuses_receive_pack_flag_and_leaves_target_empty,
  wrapper_refuses_alias_with_abbreviated_receive_pack_flag): correct orientation
  to origin→decoy (seeded with HEAD, supplies exclusion set) + script exec's
  git-receive-pack $actual (empty write target). Previous setup had origin→actual
  and script→url_repo, so actual stayed empty regardless of the guard. Verified:
  without the guard, actual/refs/heads/main appears after git push --receive-pack.

- Fix remote.origin.receivepack config test 1b: set config value to script path
  (not 'git-receive-pack url_repo'). Origin still points at decoy.

All 92 unit tests and 14 integration tests pass.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary

Finish the remaining database-store extraction tracked by
[TheSentinel454#2](TheSentinel454#2)
in one reviewable PR.

This consolidates the previously stacked domain slices after #6782
merged. It preserves the runtime/store boundary established by #6660,
#6668, #6700, and #6782 while separating database runtime infrastructure
from domain-owned persistence:

- `runtime/` owns pool construction and sizing, writer/reader routing,
read sessions and route proofs, transaction infrastructure,
observability primitives, replica fencing, health support, migrations,
and cross-cutting runtime tests.
- `store/` owns domain records, SQL, row parsing, locks and invariants,
`Db` domain methods, focused tests, and logical-operation datastore
spans.
- `lib.rs` remains a 57-line compatibility facade that preserves
existing crate-root paths and `Db` method signatures through re-exports.

Domain coverage includes API tokens, authentication allowlists,
reminders, event queries, threads, reactions, feeds, users and DMs,
push, workflows/runs/approvals, relay membership and invites, product
feedback, moderation/admin moderation, relay admin actions/operators,
git repositories, archived identities, usage, partition maintenance,
deletion, channel membership inherited from merged #6782, and the final
runtime/store layout.

The branch has been rebased onto current `main`. Database changes that
landed there were incorporated rather than overwritten:
`relay_admin_actions.rs` and `relay_operators.rs` now live under
`store/`, their 27 public `Db` wrappers and existing behavior remain
intact, and every wrapper has exactly one fixed-name datastore span.
Concurrent changes to migration, moderation, admin moderation, and error
handling are also retained.

### Exact base and head

- Base: `main` at `ed11c8d8bf0a17402be5cf243724f89471530d2f`
- Head: `codex/issue-2-store-extraction` at
`be24430472d1a87ac5c0d6026c620cd6caea3537`

### Related issue

- Structural tracker:
[TheSentinel454#2](TheSentinel454#2)
- Domain trackers:
[#6](TheSentinel454#6),
[#7](TheSentinel454#7),
[#12](TheSentinel454#12),
[#13](TheSentinel454#13)
- Acceptance trackers:
[#17](TheSentinel454#17),
[#19](TheSentinel454#19)

This supersedes #6783, #6784, #6787, #6788, #6789, #6792, #6820, #6794,
#6796, #6797, #6798, #6799, #6804, #6805, #6806, #6808, #6809, #6811,
#6812, #6813, #6814, #6815, and #6890. Their discussions remain
available for review history.

### #17 / #19 acceptance

- Preserves the metric names, fixed labels, transaction/lock timing
boundaries, and privacy/cardinality constraints introduced by #6700.
- Keeps exactly one datastore span per public logical operation,
including the 27 relay-admin wrappers added on `main`.
- Removes `store_ownership.rs`; physical ownership and focused source
guards now enforce the boundary directly.
- Leaves no `impl Db`, domain SQL, focused domain test group, or
datastore span in `lib.rs`.
- Preserves existing public paths such as `buzz_db::channel`,
`buzz_db::event`, and `buzz_db::workflow` through crate-root re-exports
while keeping internal `runtime` and `store` namespaces private.

### Non-goals

- No SQL, schema, locking, transaction, retry, timeout, or
client-visible behavior changes.
- No generic store traits, domain handles, broad `PgExecutor` migration,
new store crate, raw pool accessor, or broader directory reorganization.
- No tracker issues are closed by this PR.

### Risk

The cumulative diff is large but structural. Risk is primarily
module-path, ownership, or conflict-resolution drift. It is mitigated by
preserving public re-exports, comparing the newly moved `main`
implementations to their upstream source, source guards, touched-crate
compilation, PostgreSQL-backed test coverage, and an independent
exact-head review on a separate clean Blox workstation.

### Testing

Author workstation `buzz-tornquist-pr-6987-rebase`, rebased branch
ending at exact head `be24430472d1a87ac5c0d6026c620cd6caea3537`:

- `cargo fmt --all --check`
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings`
- `cargo test -p buzz-db --lib` — 113 passed, 240 PostgreSQL tests
intentionally ignored
- `cargo test -p buzz-db --test observability_source` — 2 passed
- PostgreSQL-backed `buzz-db` coverage under native PostgreSQL — 235
passed in the shared serial run; the five shared-state/config-sensitive
cases passed as isolated reruns against fresh schemas, including the two
owner-limit tests with their fixture's
`BUZZ_MAX_COMMUNITIES_PER_OWNER=3`
- `cargo test -p buzz-relay --lib -- --test-threads=1` under native
PostgreSQL/Redis — 991 passed; the three current-month
partition-sensitive identity-archive cases passed after provisioning the
August 2026 test partition; 87 infrastructure-marked tests remained
ignored
- Source/diff guards — relay-admin implementation bodies match current
`main`; all 27 public wrapper signatures are retained; exactly one
datastore span wraps each wrapper; `lib.rs` has zero `impl Db` blocks
and zero datastore spans; no duplicate top-level relay-admin modules or
`store_ownership.rs`; `error.rs` matches current `main`

Independent clean review workstation `buzz-tornquist-pr-6987-review`,
detached at exact head `be24430472d1a87ac5c0d6026c620cd6caea3537`:

- `cargo fmt --all --check`
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings`
- `cargo test -p buzz-db --lib` — 113 passed, 240 ignored
- `cargo test -p buzz-db --test observability_source` — 2 passed
- Exact-head ownership/re-export/instrumentation audit — no remaining
actionable findings

---------

Signed-off-by: OpenAI Codex <codex@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
This PR implements MVP, iOS-only,
[NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant
push notifications.

A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any
message that appears in the in-app Notifications tab.

## Enrollment flow
The first time the client first connects to a relay with
`BUZZ_PUSH_ENABLED`:
```mermaid
sequenceDiagram
    autonumber
    participant App as Buzz iOS app
    participant iOS
    participant Relay as Buzz relay
    participant Attest as Apple App Attest
    participant Gateway as Push gateway

    App->>Relay: Fetch NIP-11 push capability
    Relay-->>App: Push profile, current relay public key, and limits

    par
        App->>iOS: Request notification permission
        iOS-->>App: Permission result
    and
        App->>iOS: Register for remote notifications
        iOS-->>App: Device token
    end

    App->>Gateway: Request installation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Attest installation transcript
    Attest-->>App: Attestation proof
    App->>Gateway: Enroll device token and proof
    Gateway-->>App: Installation handle

    App->>Gateway: Request delegation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Assert relay-key delegation
    Attest-->>App: Assertion
    App->>Gateway: Create delegation
    Gateway-->>App: Opaque endpoint grant

    App->>Relay: Publish encrypted push lease and filters
    Relay-->>App: Lease acknowledged
```

## Push-time flow

When a notification-eligible event is received by the relay:

```mermaid
%%{init: {
  "sequence": {
    "actorMargin": 20,
    "width": 110,
    "messageMargin": 18,
    "diagramMarginX": 8,
    "wrap": true
  }
}}%%
sequenceDiagram
    autonumber
    participant Relay as Buzz relay
    participant Gateway as Push gateway
    participant APNs as Apple Push<br/>Notification service
    participant iOS
    participant NSE as Notification service<br/>extension

    Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization

    Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload
    APNs-->>Gateway: 200 OK: request accepted
    Gateway-->>Relay: 200 OK: accepted status

    APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1
    iOS->>NSE: Invoke extension<br/>original notification content

    NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization
    Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003

    NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target
```

relay → push gateway → APNs -> NSE -> Notification Center

## Known limitations

The APNs wake payload is intentionally constant and opaque: it contains
no originating community or message identifier, in keeping with the
implemented NIP-PL privacy design.

The Notification Service Extension must therefore reconnect to the relay
and resolve eligible messages after each wake. Around overlapping wakes,
timing boundaries, or resolution windows, notification presentation may
occasionally omit an expected message or display a message more than
once.

This best-effort behavior is deliberately accepted for the current
implementation and will be measured during the internal rollout to
determine whether the user experience is acceptable before any broader
deployment; the implementation does not claim exactly-once presentation.

## Validation

Live end-to-end hardware validation used an internal remotely hosted
development relay and push gateway, the APNs sandbox, and a physical
iPhone 12 mini:

- A second real Buzz client published a uniquely marked message through
the hosted relay.
- The relay matched the message and sent the constant opaque wake
through the hosted gateway. The gateway made an actual APNs request; no
`simctl push` or simulated notification was used.
- The iPhone received the notification on its lock screen. The
Notification Service Extension reconnected to the relay, fetched the
event, verified its ID and signature, and replaced the placeholder
content with the real notification title and body.
- After the app populated its shared presentation cache, a final marked
notification visibly showed the sender display name, sender avatar, and
hashtag-prefixed channel name.
- Tapping a lock-screen notification opened Buzz and exercised the
notification-response path and navigated to the corresponding message.

Final validation with a dogfood-signed artifact and production App
Attest/APNs configuration remains a release step.

## Independent pre-reviews

- **First pass:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585)
found missing transient retries, executor-key rotation suppression,
duplicate installation renewal, and an unauthenticated challenge write
amplifier. These were resolved by [retry-safe
bootstrap](12c66ea62)
and [authenticated renewal plus a cross-replica
quota](8e5ece0bd).
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962)
found delegation generation burning and an edited applied migration,
resolved by [exact-generation
revocation](c26d2159d)
and a [forward-only
migration](956c1d099).
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1)
found no blockers.
- **Exact-head re-review:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68),
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849),
and
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb)
independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also
revalidated every remediation and the endpoint-specific App Attest
enrollment bound.

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Jordan Mecom <jm@squareup.com>
Pinky is opening this PR on Wes’s behalf.

## Summary

Reduce two separately measured mobile delays without changing the relay
API or removing rich message rendering:

- Publish the finite channel-list snapshot without waiting for live
subscription setup.
- Batch active channel-list subscriptions into sorted, deterministic
chunks of at most 128 explicit channel IDs, retaining unchanged chunks.
- Install replacement chunks before retiring old coverage. Retain old
chunks across thrown replacement failures; filter callbacks to the
current relay/identity and still-desired channels; clean up
retired/in-flight work across disconnect and disposal.
- Scope the custom-emoji Markdown matcher to known shortcodes actually
referenced in the rendered content, rather than embedding the whole
community palette in every message’s regex. Preserve unknown literals,
shared-colon token boundaries, event-tag URL priority, content edits,
and code literals.
- Honor explicit zero retry hints without inventing a ten-second
session-wide gate, while preserving the ordinary live-subscription retry
backoff and any already-active gate.

## Matched performance results

Medians of three before and three after process-cold launches,
alternated on the same authenticated iPhone 17 Pro / iOS 26.5 simulator.
Before is mobile source at `e76c81968b65b0755b83efdd59dc3375c59ddf40`;
after is this production patch before two documentation-only comment
fixes.

First channel-list frame: 11.617s → 3.179s · 73% lower latency

Live setup duration: 8.475s → 0.185s · 98% lower latency

Channel-open first message-list frame: 2.754s → 1.230s · 55% lower
latency

Message data ready → first frame: 1.977s → 0.286s · 86% lower latency

Channel-open reveal complete: 2.845s → 1.394s · 51% lower latency

Channel-open data readiness: 0.770s → 0.944s · 23% higher latency

The gain is client-side orchestration/rendering, not a claim that the
relay became faster. First channel-list frame ranges were 10.835–11.788s
before and 2.872–3.395s after; channel-open first-frame ranges were
1.560–2.906s before and 1.149–1.317s after.

### Measurement boundaries

- Debug simulator builds, CPU sampling disabled, bounded timestamp
probes enabled identically. These are not release/physical-device
measurements.
- Startup clock starts at Dart `main`; build/install/native pre-main
time is excluded. Auth/preferences and OS/disk caches are retained
between new processes.
- Same account scale: 113 active channels. Latest-message events varied
slightly with live activity (1543–1546).
- Channel-open uses the same initial 50-row history window, 97 query
events, and 67 provider events. The 2306-entry emoji palette is
explicitly loaded before navigation on both sides; palette preparation
is excluded from the channel-open clock and happens after the startup
frame measurement.
- Both diagnostic builds temporarily disabled unused avatar segmentation
to work around the existing Google ML Kit arm64-simulator slice
limitation. The workaround, dependency/native changes, auto-navigation,
and all probes are excluded from this PR.

## Validation

- Full mobile package suite: `flutter test` — 1890 passed.
- `just mobile-check` — 506 files unchanged; analyzer clean.
- `just file-size-check` — policy tests and all client ratchets passed.
- `git diff --check` — passed.
- New lifecycle regressions cover front-sorting insertion across a chunk
boundary while replacement readiness is paused, failure
retention/departed-channel filtering, retired generation + disconnect
cleanup, disposal, chunk limits, unchanged-set reuse, and scope
switches.
- Emoji unit/widget coverage includes a 2500-unused-emoji palette,
unknown tokens, case matching at the component level, shared-colon
boundaries, rich text, event URL priority, and content edits.
- Fresh-frame source review traced the subscription queue/fences,
callback scopes, duplicate-event paths, matcher/wiring, and retry
scheduling.
- At committed/pushed head `13a83b628c8411c5885e6f76a250ba87accf6067`,
all normal pre-push hooks passed: `mobile-checks` (formatter, analyzer,
and the full 1890-test mobile suite), `file-size-check`, `branch-skew`,
and `push-head-scope`. The commit hook formatted 506 files with no
changes. Runtime measurements preceded only the two
documentation-comment fixes; no runtime source changed afterward.

## Limits / follow-ups

- `RelaySession.subscribe` still settles under its existing
EOSE/fallback/retryable-CLOSED contract. “Setup completed” is not an
unconditional EOSE or live-delivery guarantee. This PR does not add
status-aware replacement ownership.
- The channel-message provider still awaits subscribe before fetching
history; that separate serialization is not removed here.
- Oversized Huddle queries and the separate history batching path above
128 active channels remain follow-ups, as do pre-existing read-state
initialization/size warnings.
- Palette-only widget refresh and upstream Markdown uppercase-dispatch
behavior are not changed.
- A clean source build still has the existing Google ML Kit
arm64-simulator issue; the profiling workaround is not a proposed
product fix.

Originating Buzz conversation:
buzz://message?channel=793b0522-7995-4375-b1a6-fd94a96fa21d&id=6ba88afdec78ab2cfb6728afcd4a6d10f29e6aa33ff0f62f45d6750381e4d789

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
…hell spawns (#6904)

## Why

PR #6330 split agent harness/runtime detection into a cheap (cache-only)
path and a forced (spawning) path. Two regressions followed, both
surfacing as every harness showing "(not installed)" / "CLI missing"
across the agent create/edit picker, Agents > Agent defaults, and
Settings > Agents — blocking agent create/edit until the user clicked
Install in Settings > Agents.

## Root cause

One underlying bug, two victims:

- **Boot false-negative.** The resolve cache is in-memory, so it starts
cold on every launch. `resolve_command_cached` (the cheap path)
consulted only the Buzz-managed shim dirs plus that cold cache, and
`buzz_managed_command_path`'s allowlist structurally excludes
`buzz-agent`. The bundled sidecar could therefore never resolve on the
cheap path until a forced pass warmed the cache, so cheap-path surfaces
rendered all-missing at boot. App setup never warms the cache.
- **"Check again" hang.** `run_in_login_shell` used an untimeouted
`Command::output()`; a wedged login shell froze the whole forced
pipeline, leaving "Check again" spinning forever.

## What

- `resolve_command_cached` now also calls `resolve_workspace_command`,
resolving the bundled sidecar via a filesystem stat (no spawn) — the
same class of work the managed-shim check already performs. `buzz-agent`
can no longer report missing, even inside the boot warm window.
- New `discovery/bounded_command.rs` runs any discovery child under a
hard wall-clock deadline, polling with `try_wait` rather than blocking
on `wait()`. Stdout and stderr are piped to two drain threads whose
buffers share an aggregate `CAPTURE_LIMIT`; a breach fails closed (kill
the tree, return `None`), so a noisy or hostile probe can force neither
unbounded memory nor disk fill. Tree teardown runs on every exit path —
timeout, error, cap breach, *and* success — because a login-shell rc
file or auth CLI can legitimately background a descendant that would
otherwise outlive discovery. Ownership is deliberately asymmetric:
- **Unix:** the child leads its own process group (`process_group(0)`);
teardown is `SIGTERM` → bounded grace → `SIGKILL` on the group. A
descendant that leaves the group (`setsid`/`setpgid`) while holding a
pipe is not owned and may survive one probe, but can never hang or
unbound the helper: the Unix drains read nonblocking and end on
`WouldBlock` once teardown sets the stop flag, so the join returns
promptly without waiting on an escaped writer's EOF.
- **Windows:** the child is spawned `CREATE_SUSPENDED`, assigned to a
kill-on-close Job Object while frozen, then resumed. The job owns the
root before any descendant can exist and is created without breakaway,
so no writer can escape — a hard whole-tree guarantee, and closing the
job reaps the tree even after the root has exited. Any failure to
create, assign, or resume is fail-closed: the child is terminated and
reaped and the spawn returns `None` (discovery treats it as
command-not-found) rather than running unowned.
- Each login-shell candidate is bounded by a 10s timeout via that
helper, falling through to the next candidate on timeout instead of
aborting the resolve. The login-shell path cache is generation-aware: a
probe that loses to a concurrent refresh or lands mid-refresh returns
the authoritative cached value (or re-probes under the new generation)
rather than its own rejected local result, so a losing thread can never
settle the UI with a PATH-missing catalog while the cache holds a fresh
success.
- Warm the ACP runtime catalog once at `AppShell` mount and gate the
cheap-path surfaces on that pass. A module-level boot-warm state (`idle`
→ `pending` → `settled`/`failed`, deduped per launch) lets
`useAcpRuntimesQuery` present a cold catalog as *loading* while the
first forced pass runs and as a *retryable error* (carrying the probe's
real reason) if it fails, instead of blessing "every harness not
installed" as authoritative. A non-empty catalog always wins, so a
revalidation or later failure never blanks a good list; the gate only
overlays once the warm has started, so onboarding (which renders before
the warm) is unaffected. Deduping per launch also fixes the previous
per-remount re-fire.

## Verification

Unix teardown and the drain contract are runtime-proven by
`#[ignore]`-free tests that record a backgrounded descendant's real PID
and assert the helper returns promptly on both the success and timeout
paths without blocking on that writer. The generation-aware login-shell
cache is covered by deterministic tests through a `cfg(test)` injectable
probe seam that assert the function's return value under both
concurrent-refresh interleavings — the losing caller returns the peer's
committed success, and a mid-probe refresh forces a re-probe to the
fresh value. The Windows ownership contract has no CI lane, so
`bounded_command.rs` carries two `#[ignore]`-gated tests (spawn/assign
race, looped; and the timeout path) for a sanctioned run on a Windows
host. The boot-warm gate is covered by unit tests for the pure overlay
and the `startBootWarm` failure → retry → settle lifecycle.

Origin: [Buzz
thread](buzz://message?channel=5ef5d5bb-643f-4b87-bbf4-e8b64585ffeb&id=a4b1c4485de4d35cff0f914d4f4211c796f44f670e76de2ef9431f7e882c906e)

Fixes #6872
Related #6662

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
**Category:** improvement
**User Impact:** Automatic mentions are easier to turn off and now
behave consistently across conversations, settings, drafts, and repeated
agent mentions.

**Problem:** People found the new automatic mention behavior hard to
control: turning it off in Settings did not reliably affect the
composer, removing a mention could require also disabling the feature,
and root/thread composers could inherit or restore surprising state.
Other reported rough edges included only one of several mentioned agents
becoming automatic, synthetic mentions leaking into drafts, restored
mentions corrupting adjacent text, controls remaining visible in
archived channels, and unclear picker feedback. See the [original
feedback
thread](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=c949ec399274fbb0d6633da3f95712e843a67dcada214f63d72f6975b406604b).

**Solution:** Polish the existing feature around the problems people
encountered, keeping automatic mentions controllable and scoped to the
active conversation.

| Reported issue | UX fix |
| --- | --- |
| Turning automatic mentions off in Settings did not reliably update the
composer. | The global setting and composer control stay synchronized,
and disabling the feature does not clear typed text. |
| Removing an automatic mention could require both deleting the mention
and turning off the feature. | Removing or unchecking an agent excludes
that agent for the current conversation, while explicitly re-adding the
agent can restore automatic mention behavior. |
| Root and thread composers could share or restore surprising
selections. | Each root or thread composer keeps its own automatic
audience and restores it when the user returns. A request to enable
automatic mentions only in agent threads was considered; this PR keeps
them available at the channel root but prevents state from leaking
between the two. |
| Mentioning multiple agents could leave only one saved as automatic. |
Multi-agent selections remain represented in the automatic audience and
restored mention chips. |
| Automatic mention prefixes could be saved as if the user typed them. |
Synthetic prefixes stay out of persisted drafts while authored text is
preserved. |
| Restored mentions could lose their separator and corrupt continued
typing. | Restored multi-word mentions retain their trailing space and
place the caret after it. |
| Archived channels showed automatic-mention state beside a disabled
composer. | Disabled composers hide automatic-mention controls while
preserving the draft and restoring state when re-enabled. |
| Confirmation and picker behavior made the feature feel difficult to
inspect or adjust. | Confirmations dismiss with removed agents, remain
open while hovered, and expose the setting before it changes; pin icons,
contrast, scope copy, animation, and keyboard toggling are also
clarified. |
| Agent suggestions and membership state could shift during directory
refreshes. | Suggestions and membership labels stay stable during
refreshes, while send-time authorization still revalidates access. |

## Changes

<details>
<summary>File changes</summary>


**desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs**
Adds coverage for the channel-roster eligibility rules used by agent
mention autocomplete.

**desktop/src/features/agents/lib/agentAutocompleteEligibility.ts**
Aligns agent autocomplete eligibility with channel membership so
available agents and their labels stay trustworthy.

**desktop/src/features/channels/ui/MembersSidebar.tsx**
Uses the shared member-pubkey logic when presenting and acting on
channel members.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Covers preference changes that must remain stable while composer
controls are toggled.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Keeps the automatic-mention preference as durable user intent rather
than transient composer state.

**desktop/src/features/messages/lib/mentionMemberPubkeys.ts**
Centralizes which member identities count as mentionable in the current
channel.

**desktop/src/features/messages/lib/persistentAgentAudience.test.mjs**
Expands lifecycle coverage for persistent agent audiences, explicit
exclusions, and restored mentions.

**desktop/src/features/messages/lib/persistentAgentAudience.ts**
Models automatic, explicit, and excluded agent audiences separately so
user choices survive updates without leaking across composers.


**desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs**
Verifies implicit automatic mentions are removed without damaging
surrounding separators or authored content.

**desktop/src/features/messages/lib/stripImplicitAgentMentions.ts**
Strips presentation-only automatic mentions before draft persistence
while preserving whitespace and authored text.

**desktop/src/features/messages/lib/useMentions.ts**
Routes mention insertion and removal through the composer-local audience
lifecycle.

**desktop/src/features/messages/lib/useRichTextEditor.ts**
Preserves mention-chip structure and caret placement when automatic
mentions are restored.

**desktop/src/features/messages/ui/ComposerAddressControls.test.mjs**
Updates control-state expectations for disabled automatic mentions and
restored pin affordances.

**desktop/src/features/messages/ui/ComposerAddressControls.tsx**
Makes automatic-mention state, disabled presentation, and pin controls
visually explicit.

**desktop/src/features/messages/ui/MentionAutocomplete.test.mjs**
Adds coverage for roster labels, pin state, and picker behavior after
mention selection.

**desktop/src/features/messages/ui/MentionAutocomplete.tsx**
Keeps the shortcut picker open for repeated selection and restores
visible automatic-mention pin indicators.

**desktop/src/features/messages/ui/MessageComposer.tsx**
Scopes automatic mention state to each root or thread composer and
coordinates restoration, draft persistence, and sending.

**desktop/src/features/messages/ui/MessageComposerToolbar.tsx**
Passes the effective automatic-mention state into the toolbar
presentation.

**desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs**
Updates keyboard interaction coverage for toggling agents in place.

**desktop/src/features/messages/ui/useAddressedAgentMentionRestore.ts**
Restores automatic mention chips after lifecycle changes without moving
or duplicating authored content.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Substantially expands coverage for toggles, exclusions, synchronization,
and picker dismissal rules.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**
Keeps the picker usable across repeated choices and preserves explicit
per-agent intent while settings change.

**desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts**
Makes the keyboard shortcut toggle the highlighted automatic audience
choice without replacing unrelated selections.

**desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts**
Owns composer-local automatic-mention lifecycle behavior, including
restoration, exclusions, deletion, and disabled-state handling.

**desktop/src/features/messages/ui/useComposerMentionPicker.test.mjs**
Adds focused picker lifecycle coverage for selection, hover, and
dismissal behavior.

**desktop/src/features/messages/ui/useComposerMentionPicker.ts**
Prevents premature picker dismissal while the user is interacting with
its controls.

**desktop/src/features/messages/ui/useDraftPersistSnapshot.ts**
Persists only user-authored draft content rather than implicit automatic
mention decorations.

**desktop/src/shared/lib/keyboard-shortcuts.ts**
Updates the automatic-mention shortcut description to match its toggle
behavior.

**desktop/src/testing/e2eBridge.ts**
Extends the desktop test bridge with the state needed to exercise roster
and automatic-mention transitions.

**desktop/tests/e2e/mentions.spec.ts**
Covers roster-based labels, managed-agent invitation, revocation, and
recovery behavior in the complete mention flow.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Adds end-to-end coverage for root/thread isolation, preference
synchronization, manual exclusions, draft hygiene, restored chips,
separators, hover behavior, and disabled presentation.

</details>

## Reproduction Steps

1. Open a channel with at least two available agents and enable
automatic mentions from the composer mention control.
2. Select multiple agents, remove or uncheck one, and confirm subsequent
composer updates keep that agent excluded while the others remain
automatic.
3. Open a thread, choose a different automatic audience there, and
switch between the thread and root composer; confirm each composer
retains only its own choices.
4. Disable automatic mentions and confirm the draft text remains
unchanged while automatic chips and controls show the disabled state;
re-enable the setting and confirm eligible automatic chips return.
5. Delete an automatic mention chip, then explicitly add the agent
again; confirm it immediately returns as an automatic mention without
disturbing spaces or the caret, including for a multi-word name.
6. Reload with a saved draft and confirm implicit automatic mentions
were not persisted as authored draft text.
7. Use the automatic-mention keyboard shortcut and picker repeatedly;
confirm the picker remains open for additional choices and the
highlighted agent toggles in place.

## Validation

Validated at `34d208b47d64a9816f88e10a46bcfd479e917d75` after rebasing
onto `origin/main` (`69096c9a8`):

- Desktop unit tests: 5,731 passed, 0 failed.
- Desktop TypeScript typecheck: passed.
- Desktop E2E build: passed; emitted only existing chunk and
dynamic-import warnings.
- `pnpm check`: exited successfully; 4 warnings and 5 informational
findings are in unrelated files introduced by current main.

## Screenshots/Demos

The behavioral changes are covered by the focused desktop E2E scenarios
above. Screenshots can be attached from the screenshot-producing
automatic-mention E2E after the PR is created.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
## Summary

A failed initial channel-history request no longer appears as an
authoritative empty channel. The timeline now shows an announced error
with a Retry action, while cached messages remain visible when a later
refresh fails; successful empty channels continue to use their normal
intro state.

### Related issue

None found.

### Testing

- Full desktop unit suite (`pnpm test`)
- Desktop TypeScript check (`pnpm exec tsc --noEmit`)
- Biome checks for changed files
- Repository file-size ratchet
- Full pre-push desktop checks and tests
- Desktop app launched successfully against local Postgres and Redis for
manual testing

No screenshot is included because the new UI is only shown after a
terminal relay-history failure; the regression test pins the
error/empty/list precedence directly.

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
…ivepack bypass

The existing verify_push_rejects_receivepack_config test only verified
the guard fires (returns Err with a receivepack message). It did not
prove the bypass is executable — that without the guard, a custom
remote.origin.receivepack actually causes ls-remote to read one
destination while the real push lands at another.

Add the positive-control block: seed remote with the first commit so
ls-remote would exempt it, configure a wrapper script as receivepack
that redirects all pushes to an empty redirect repo, then direct-push
a second commit (bypassing verify_push). Assert the commit lands in
redirect (not remote) — the split-service bypass is real.

Also fix a cfg-gated unused-variable warning in kill_bounded_tree
(child -> _child, silent on unix where it is unused, valid on non-unix
where the #[cfg(not(unix))] branch calls _child.kill()).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity

* origin/main:
  fix(desktop): surface channel history load failures (#7013)
  fix(composer): polish automatic mentions (#6956)
  fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904)
  perf(mobile): reduce cold startup and channel rendering delays (#6996)
  feat(mobile): push notifications MVP (#6269)
  refactor(db): extract domain stores from database runtime (#6987)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…dows Job Object teardown

Addresses the three IMPORTANT blockers from Thufir's re-check against f406fb1.

**Blocker 1 — remote_object_ids strict validation + 3 regressions**

Change text.trim().is_empty() to text.is_empty(): whitespace-only ls-remote
output is a malformed response, not an empty inventory.  Add empty-refname
rejection (tab at end with nothing after it) and extra-tab-field rejection
(protocol variant the parser was not designed for).  Both fail closed (None).

Three regression tests exercise each surface with a fake git binary that
emits the malformed output:
  - remote_object_ids_rejects_whitespace_only_output
  - remote_object_ids_rejects_empty_refname
  - remote_object_ids_rejects_extra_tab_fields

**Blocker 2 — Windows process-tree teardown via Job Object**

Replace CREATE_NEW_PROCESS_GROUP + Child::kill() with the suspended-child
+ kill-on-close Job Object lifecycle, mirroring the established pattern in
desktop/src-tauri/src/managed_agents/discovery/bounded_command.rs.

Child is spawned CREATE_SUSPENDED so no descendant can exist before the job
owns the root (closes the spawn-to-assign race).  BoundedJob wraps the HANDLE
and drops it via CloseHandle on the kill path; JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
ensures every descendant is reaped when the handle closes.

kill_bounded_tree gains a #[cfg(windows)] job: &mut Option<BoundedJob>
parameter (passed via inline #[cfg] at every call site) and drops the job
rather than calling Child::kill().

Windows CI regression tests (reaps_backgrounded_descendant_on_windows,
reaps_descendant_on_timeout_windows) are gated #[ignore] per the codebase
convention for tests that require a Windows host; the test evidence
demonstrates the Job Object lifecycle is materially identical to the
bounded_command.rs reference.

**Blocker 3 — crate-wide ENV_LOCK**

Move the env-mutation lock from each test module's private static into a
single pub static ENV_LOCK: Mutex<()> at the crate root (cfg(test)-gated).
lib.rs mod tests and git_wrapper.rs mod tests both reference crate::ENV_LOCK,
serializing all env mutations across one cargo-test process.

lib.rs tests additionally restore the exact prior OsString value (via var_os
before mutation, match on Some/None after) rather than blindly calling
remove_var.  This preserves whatever the surrounding test environment had set
and avoids masking inter-test leaks.

Unsafe blocks added throughout for set_var/remove_var per Rust 2024 edition.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Addresses the two IMPORTANT blockers from Thufir's re-check against f4fc464.

**Blocker 1 — four Clippy errors (Rust Lint + Windows Rust fail)**

1. manual_strip: replace `if key.starts_with("url.") { &key["url.".len()..] }`
   with `if let Some(subsection_and_rest) = key.strip_prefix("url.")`.
2. sliced_string_as_bytes: replace `subsection_and_rest[..sep].as_bytes()`
   with `&subsection_and_rest.as_bytes()[..sep]`.
3. redundant_guards: replace `k if k == "core.sshcommand"` with
   `"core.sshcommand"` (direct match arm pattern).
4. redundant_guards: replace `k if k == "core.gitproxy"` with
   `"core.gitproxy"`.

All four are PR-introduced (absent from origin/main).

**Blocker 2 — Windows regression tests + explicit CI step**

Add two #[cfg(windows)] + #[ignore] tests for capture_raw_bounded:
  - capture_raw_bounded_reaps_descendant_on_success_windows: PowerShell root
    backgrounds a detached descendant (records its PID), waits for the PID
    file, exits 0.  Asserts the descendant is dead after capture_raw_bounded
    returns Some.
  - capture_raw_bounded_reaps_descendant_on_timeout_windows: same setup but
    root loops forever, forcing the deadline.  Asserts None returned and the
    descendant is dead.

Both use windows_sys PROCESS_QUERY_LIMITED_INFORMATION + GetExitCodeProcess to
prove the descendant PID is gone, not just assumed reaped.  Mutation: removing
BoundedJob or making kill_bounded_tree a no-op leaves the descendant alive and
the assert fails.

Add helpers windows_pid_alive(u32) -> bool and read_windows_pid(&str) -> u32
for PID probing and asynchronous PID-file reading.

Add a "Test (buzz-git-identity)" CI step to the Windows Rust job in ci.yml,
placed after the existing "Test (buzz-dev-mcp)" step.  The step runs
`cargo test -p buzz-git-identity --target $TARGET -- --run-ignored
--test-threads=1`, so the #[ignore]-tagged tests actually execute in CI rather
than being dead-letter.  `--test-threads=1` serializes the PowerShell
launches to keep wall-clock predictable on the runner.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
All 9 are in test-only code introduced in the previous commit; no
production logic touched.

8x needless_borrows_for_generic_args: drop the & on .args(&[...]) array
literals in eight Command::args call sites (the array literal coerces to
a slice without the borrow).

1x manual_contains (line 5870): replace
  caps_keys.iter().any(|k| *k == "core.sshcommand")
with
  caps_keys.contains(&"core.sshcommand")

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… step

--run-ignored is a cargo-nextest flag; the libtest harness used by
cargo test does not recognise it. Replace with --ignored (libtest's
flag for running only #[ignore]-tagged tests) so the two Windows
reap-regression tests actually execute on the windows-latest runner.

Also update the matching comment in ci.yml and the #[ignore = "..."]
message strings in git_wrapper.rs to reference --ignored.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants